package cmds

import (
	"bufio"
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	log "github.com/cihub/seelog"
	"github.com/dustin/go-humanize"
	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"reflect"

	"runtime"
	"strconv"
	"strings"
	"time"
	"wrench/common"
	"wrench/config"
	"wrench/render"
)

type hostDesc struct {
	Ip       string
	HostName string
	Group    string
	User     string
	SshPort  int
}

func OrderTask(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	logger, err := log.LoggerFromConfigAsBytes([]byte(config.LogConfig))
	if err != nil {
		return
	}
	err = log.ReplaceLogger(logger)
	if err != nil {
		return
	}
	defer log.Flush()

	switch moduleType(tasks) {
	case "[]config.Shell":
		TaskCmd(tasks, yaml, swg)
		return
	case "[]config.Rsync":
		TaskFileSync(tasks, yaml, swg)
		return
	case "[]config.Render":
		TaskFileRender(tasks, yaml, swg)
		return
	case "[]config.File":
		TaskFileJob(tasks, yaml, swg)
		return
	case "[]config.Script":
		TaskScript(tasks, yaml, swg)
		return
	case "[]config.Plugin":
		TaskPlugin(tasks, yaml, swg)
		return
	default:
		err = log.Warn(tasks, " ", reflect.TypeOf(tasks).Field(1).Type.String(), " module not exist.\n")
		if err != nil {
			return
		}
		return
	}

}

func moduleType(t *config.Task) string {
	for i := 0; i < reflect.ValueOf(*t).NumField(); i++ {
		if reflect.ValueOf(*t).Field(i).Len() > 0 {
			//fmt.Println(reflect.ValueOf(t).Field(i).Interface())
			return reflect.ValueOf(*t).Field(i).Type().String()
		}
	}
	return ""
}

func TaskPlugin(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	for i := range yaml.HostList {
		swg.CPUChannel <- struct{}{}

		if !yaml.HostList[i].Include {
			swg.HostWg.Done()
			<-swg.CPUChannel
			continue
		}
		go runPlugin(tasks, swg, &yaml.HostList[i])
	}
	swg.HostWg.Wait()
}

func runPlugin(task *config.Task, swg *config.SyncWaitGroupChannelType, server *config.HostList) {
	config.CreateClient(server)

	var err, scpErr, runErr error
	for _, Plugins := range task.Plugin {

		if server.Session, err = CreateSession(server); err != nil {
			panic(err)
		}

		var cmd, outs, Status, Error string

		log.Trace("[plugin] ", server.SshIp, " create dir: ", "/tmp/.plugin/"+Plugins.Name)
		err = server.SftpClient.MkdirAll("/tmp/.plugin/" + Plugins.Name)
		if err != nil {
			return
		}

		if scpErr = SentAll(server, config.TddhHome+"plugins/"+Plugins.Name, "/tmp/.plugin/"+Plugins.Name, "", 4096, ""); scpErr != nil {
			Status = "Failed"
			Error = scpErr.Error()
			goto copyError
		}

		if Plugins.Env == "" {
			Plugins.Env = "/bin/sh"
		}

		Plugins.Var = append(Plugins.Var, hostEnv(server.SshGroup, server.SshIp)...)
		cmd = "export " + strings.Join(Plugins.Var, " ") + "; cd /tmp/.plugin/" + Plugins.Name + " ;" + Plugins.Env + " " + Plugins.Name + " ; rm -rf " + "/tmp/.plugin/" + Plugins.Name
		log.Trace("[plugin] ", server.SshIp, " cmd: ", cmd)

		outs, runErr = RunShell(server.Session, server, cmd)

		if runErr != nil {
			Status = "Failed"
			Error = runErr.Error()
		} else {
			Status = "Success"
		}

	copyError:
		cmdResult := struct {
			EName  string
			Plugin config.Plugin
			Result string
			Error  string
			Status string
		}{
			EName:  Plugins.Name,
			Plugin: Plugins,
			Result: outs,
			Error:  Error,
			Status: Status,
		}

		res, err := json.Marshal(cmdResult)
		check(err)
		var out bytes.Buffer
		err = json.Indent(&out, res, "", " ")
		check(err)

		if runErr != nil || scpErr != nil {
			_ = log.Error("[", server.SshIp, "] => \n", out.String(), "\n")
		} else {
			log.Info("[", server.SshIp, "] => \n", out.String(), "\n")
		}

	}
	swg.HostWg.Done()
	log.Flush()
	<-swg.CPUChannel
}

func TaskScript(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	for i := range yaml.HostList {
		swg.CPUChannel <- struct{}{}

		if !yaml.HostList[i].Include {
			swg.HostWg.Done()
			<-swg.CPUChannel
			continue
		}
		go RunScript(tasks, swg, &yaml.HostList[i])
		log.Flush()
	}
	swg.HostWg.Wait()
}

func RunScript(task *config.Task, swg *config.SyncWaitGroupChannelType, server *config.HostList) {
	defer log.Flush()
	config.CreateClient(server)
	var err error

	for _, Script := range task.Script {

		var outs, Status, Error string
		if !CheckFileIsExist(Script.File) {
			Status = "failed"
			Error = Script.File + "not exist"
		}

		if server.Session, err = CreateSession(server); err != nil {
			panic(err)
		}

		if Script.Chdir == "" {
			Script.Chdir = "/tmp"
		}
		log.Trace("[script] ", server.SshIp, " chdir: ", Script.Chdir)

		if Script.Env == "" {
			Script.Env = "/bin/sh"
		}
		log.Trace("[script] ", server.SshIp, " env: ", Script.Env)
		_, file := filepath.Split(Script.File)

		log.Trace("[script] ", server.SshIp, " file: ", Script.Chdir+"/"+file)
		e := SentAll(server, Script.File, Script.Chdir, "", 4096, "")
		if e != nil {
			_ = log.Error("[script] ", server.SshIp, " file: ", Script.Chdir+"/"+file, " ", e)
			return
		}

		Script.Var = append(Script.Var, hostEnv(server.SshGroup, server.SshIp)...)

		cmd := "export " + strings.Join(Script.Var, " ") + ";" + Script.Env + " " + path.Join(Script.Chdir, file) + " ; rm -rf " + path.Join(Script.Chdir, file)
		log.Trace("[script] ", server.SshIp, " command: ", cmd)

		outs, runErr := RunShell(server.Session, server, cmd)

		if runErr != nil {
			Status = "Failed"
			Error = runErr.Error()
		} else {
			Status = "Success"
		}

		cmdResult := &struct {
			EName  string
			Script config.Script
			Result string
			Error  string
			Status string
		}{
			EName:  Script.Name,
			Script: Script,
			Result: outs,
			Error:  Error,
			Status: Status,
		}

		res, err := json.Marshal(cmdResult)
		check(err)
		var out bytes.Buffer
		err = json.Indent(&out, res, "", " ")
		check(err)

		if runErr != nil {
			_ = log.Error("[", server.SshIp, "] => \n", out.String(), "\n")
		} else {
			log.Info("[", server.SshIp, "] => \n", out.String(), "\n")
		}

	}

	swg.HostWg.Done()
	<-swg.CPUChannel

}

func TaskFileRender(tasks *config.Task, yaml *config.Yaml, sw *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	sw.HostWg.Add(len(yaml.HostList))
	for i := range yaml.HostList {
		sw.CPUChannel <- struct{}{}
		if !yaml.HostList[i].Include {
			sw.HostWg.Done()
			<-sw.CPUChannel
			continue
		}
		go runFileRender(tasks, sw, &yaml.HostList[i])
	}
	sw.HostWg.Wait()
	log.Flush()
}

func runFileRender(tasks *config.Task, s *config.SyncWaitGroupChannelType, server *config.HostList) {
	defer log.Flush()
	Status := "Success"
	RenderErr := ""
	var renderErr error

	for _, Renders := range tasks.Render {
		if checkFileType(Renders.Src) == ".j2" {
			renderS := render.ConfigRenders{
				Name: Renders.Name,
				Src:  Renders.Src,
				Dst:  Renders.Dst,
				Temp: Renders.Temp,
			}
			Renders.Dst, renderErr = renderS.Render(server, Renders.Name)
			if renderErr != nil {
				RenderErr = renderErr.Error()
				Status = "Failed"
			}
		}

		cmdResult := struct {
			EName   string
			Renders config.Render
			Error   string
			Status  string
		}{
			EName:   Renders.Name,
			Renders: Renders,
			Error:   RenderErr,
			Status:  Status,
		}

		res, jsonErr := json.Marshal(cmdResult)
		check(jsonErr)
		var out bytes.Buffer
		jsonErr = json.Indent(&out, res, "", " ")
		check(jsonErr)

		if Status == "Failed" {
			_ = log.Error("[", server.SshIp, "] => \n", out.String(), "\n")
		} else {
			log.Info("[", server.SshIp, "] => \n", out.String(), "\n")
		}

	}
	s.HostWg.Done()
	<-s.CPUChannel
}

func TaskFileJob(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	for i := range yaml.HostList {
		swg.CPUChannel <- struct{}{}

		if !yaml.HostList[i].Include {
			swg.HostWg.Done()
			<-swg.CPUChannel
			continue
		}
		go runFileJob(tasks, swg, yaml, &yaml.HostList[i], i)
	}
	swg.HostWg.Wait()
	log.Flush()
}

func runFileJob(tasks *config.Task, sg *config.SyncWaitGroupChannelType, yaml *config.Yaml, server *config.HostList, n int) {
	Status := "Success"
	CopyErr := ""
	config.CreateClient(server)
	var err error
	if server.Session, err = CreateSession(server); err != nil {
		panic(err)
	}
	var copyErr error

	for _, file := range tasks.File {
		fmt.Println("file: ", reflect.TypeOf(file))
		if reflect.TypeOf(file).String() == "config.File" {
			fmt.Println(true)
		}
		if file.Src == "" || file.Dst == "" {
			log.Debug("file is nil")
			CopyErr = errors.New("value cannot be null\"").Error()
			Status = "Failed"
			goto result
		}

		if file.Fetch {
			log.Debug("[fetch] ", server.SshIp, " file : ", file.Src, " ", file.Dst, " ", file.Buf)
			if copyErr = FetchAll(server, file.Src, file.Dst, file.Buf, file.Exclude); copyErr != nil {
				Status = "Failed"
				CopyErr = copyErr.Error()
			}
			goto result
		}

		if file.Source == "" {
			log.Debug("[sent] ", server.SshIp, " file sent: ", file.Src, " ", file.Dst, " ", file.Temp, " ", file.Buf)
			if copyErr = SentAll(server, file.Src, file.Dst, file.Temp, file.Buf, file.Exclude); copyErr != nil {
				Status = "Failed"
				CopyErr = copyErr.Error()
			}
			goto result
		}

		if file.Source != "" {
			sn := sourceConnect(yaml, file.Source)
			if sn == n {
				continue
			}
			log.Debug("[transfer] ", server.SshIp, " transfer: ", file.Src, " -> ", server.SshIp, " ", file.Dst)
			if copyErr = TransferAll(server, file.Src, file.Dst, file.Temp, file.Buf, file.Exclude, &yaml.HostList[sn]); copyErr != nil {
				Status = "Failed"
				CopyErr = copyErr.Error()
			}
		}

	result:
		cmdResult := struct {
			EName  string
			Files  config.File
			Error  string
			Status string
		}{
			EName:  file.Name,
			Files:  file,
			Error:  CopyErr,
			Status: Status,
		}
		res, jsonErr := json.Marshal(cmdResult)
		check(jsonErr)
		var out bytes.Buffer
		jsonErr = json.Indent(&out, res, "", " ")
		check(jsonErr)

		if Status == "Failed" {
			_ = log.Error("[", server.SshIp, "] => \n", out.String(), "\n")
		} else {
			log.Info("[", server.SshIp, "] => \n", out.String(), "\n")
		}

	}
	sg.HostWg.Done()
	log.Flush()
	<-sg.CPUChannel
}

func sourceConnect(yaml *config.Yaml, s string) (n int) {
	var server config.HostList
	for n, server = range yaml.HostList {
		if server.SshName == s {
			config.CreateClient(&yaml.HostList[n])
			break
		}
	}
	return n
}

func TransferAll(server *config.HostList, localFile string, remoteDir string, varFile string, size int, exclude string, source *config.HostList) error {
	defer log.Flush()
	err := server.SftpClient.MkdirAll(remoteDir)
	if err != nil {
		return err
	}

	fileInfo, e := source.SftpClient.Stat(localFile)
	if e != nil {
		log.Trace("[transfer] ", source.SshIp, " stat ", fileInfo.Name(), " ", e.Error())
		return err
	}

	if !fileInfo.IsDir() {
		if common.Filter(strings.Split(exclude, ";"), fileInfo.Name()) {
			log.Debug("[transfer] ", server.SshIp, " exclude ", fileInfo.Name())
			return nil
		}

		e = TransferFile(server, localFile, remoteDir, size, fileInfo.Mode(), source)
		if e != nil {
			return e
		}
		return nil
	}

	fileList, readDirErr := source.SftpClient.ReadDir(localFile)
	if readDirErr != nil {
		log.Debug("[transfer] ", server.SshIp, " open file:", localFile, " ", readDirErr.Error())
		return readDirErr
	}

	for _, file := range fileList {
		if common.Filter(strings.Split(exclude, ";"), file.Name()) {
			log.Debug("[transfer] ", server.SshIp, " exclude ", file.Name())
			continue
		}

		if file.IsDir() {
			log.Debug("[transfer] ", server.SshIp, " create dir: ", path.Join(remoteDir, file.Name()))
			e = server.SftpClient.MkdirAll(path.Join(remoteDir, file.Name()))
			if e != nil {
				return e
			}

			log.Trace("[transfer] ", server.SshIp, " chmod dir: ", path.Join(remoteDir, file.Name()), " ", file.Mode().String())
			e = server.SftpClient.Chmod(path.Join(remoteDir, file.Name()), file.Mode())
			if e != nil {
				_ = log.Warn("[transfer] ", server.SshIp, " chmod dir: ", path.Join(remoteDir, file.Name()), " ", e.Error())
				continue
			}
			e = TransferAll(server, path.Join(localFile, file.Name()), path.Join(remoteDir, file.Name()), varFile, size, exclude, source)
			if e != nil {
				log.Trace("[transfer] ", server.SshIp, " TransferAll: ", path.Join(remoteDir, file.Name()), " ", e.Error())
				continue
			}
		} else {
			log.Trace("[transfer] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()))
			f, e1 := source.SftpClient.Stat(path.Join(localFile, file.Name()))
			if e1 != nil {
				_ = log.Warn("[transfer] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()), " ", e1.Error())
				continue
			}
			e = TransferFile(server, path.Join(localFile, file.Name()), remoteDir, size, f.Mode(), source)
			if e != nil {
				_ = log.Warn("[transfer] ", server.SshIp, " TransferFile: ", path.Join(localFile, file.Name()), " ", e1.Error())
				continue
			}
		}
	}

	return nil
}

func SentAll(server *config.HostList, localFile string, remoteDir string, varFile string, size int, exclude string) error {
	defer log.Flush()
	err := server.SftpClient.MkdirAll(remoteDir)
	if err != nil {
		return err
	}
	fileInfo, er := os.Stat(localFile)
	if er != nil {
		_ = log.Error("[sent] localhost", " stat info: ", localFile, " ", er.Error())
		return nil
	}

	if !fileInfo.IsDir() {
		if common.Filter(strings.Split(exclude, ";"), fileInfo.Name()) {
			log.Debug("[sent] ", server.SshIp, " exclude ", fileInfo.Name())
			return nil
		}

		err = SentFile(server, localFile, remoteDir, varFile, size, fileInfo.Mode())
		if err != nil {
			return err
		}
		return nil
	}

	fileList, readDirErr := os.ReadDir(localFile)
	if readDirErr != nil {
		return readDirErr
	}
	for _, file := range fileList {

		if common.Filter(strings.Split(exclude, ";"), file.Name()) {
			log.Debug("[sent] ", server.SshIp, " exclude ", file.Name())
			continue
		}

		if file.IsDir() {
			log.Debug("[sent] ", server.SshIp, " create dir: ", path.Join(remoteDir, file.Name()))
			err = server.SftpClient.MkdirAll(path.Join(remoteDir, file.Name()))
			if err != nil {
				return err
			}

			log.Trace("[sent] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()))
			f, e := os.Stat(path.Join(localFile, file.Name()))
			if e != nil {
				_ = log.Warn("[sent] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()), " ", e.Error())
				continue
			}

			log.Trace("[sent] ", server.SshIp, " chmod dir: ", path.Join(remoteDir, file.Name()), " ", f.Mode().String())
			err = server.SftpClient.Chmod(path.Join(remoteDir, file.Name()), f.Mode())
			if err != nil {
				_ = log.Warn("[sent] ", server.SshIp, " chmod dir: ", path.Join(remoteDir, file.Name()), " ", f.Mode().String(), " ", err.Error())
				continue
			}

			err = SentAll(server, path.Join(localFile, file.Name()), path.Join(remoteDir, file.Name()), varFile, size, exclude)
			if err != nil {
				continue
			}
		} else {
			if common.Filter(strings.Split(exclude, ";"), file.Name()) {
				log.Debug("[sent] ", server.SshIp, " exclude ", file.Name())
				continue
			}

			log.Trace("[sent] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()))
			f, e := os.Stat(path.Join(localFile, file.Name()))
			if e != nil {
				_ = log.Warn("[sent] ", server.SshIp, " stat info: ", path.Join(localFile, file.Name()), " ", e.Error())
				continue
			}

			err = SentFile(server, path.Join(localFile, file.Name()), remoteDir, varFile, size, f.Mode())
			if err != nil {
				continue
			}
		}
	}

	return nil
}

func TransferFile(server *config.HostList, localFile string, remoteDir string, buf int, perm os.FileMode, source *config.HostList) (err error) {
	defer log.Flush()
	log.Trace("[transfer] ", source.SshIp, " open file: ", localFile)
	srcFile, e := source.SftpClient.Open(localFile)
	if err != nil {
		_ = log.Warn("[transfer] ", source.SshIp, " open file: ", localFile, " ", e.Error())
		return err
	}
	_, file := filepath.Split(localFile)

	log.Debug("[transfer] ", server.SshIp, " file: ", remoteDir+file)
	dstFile, _ := server.SftpClient.Create(remoteDir + file)

	log.Trace("[transfer] ", server.SshIp, " chmod file: ", remoteDir+file, " ", perm.String())
	err = server.SftpClient.Chmod(remoteDir+file, perm)
	if err != nil {
		return err
	}

	defer func(srcFile *sftp.File) {
		err1 := srcFile.Close()
		if err1 != nil {
			_ = log.Warn("[transfer] ", server.SshIp, " close file: ", srcFile.Name(), " ", err1.Error())
		}
	}(srcFile)

	defer func(dstFile *sftp.File) {
		err1 := dstFile.Close()
		if err1 != nil {
			_ = log.Warn("[transfer] ", server.SshIp, " close file: ", dstFile.Name(), " ", err1.Error())
		}
	}(dstFile)

	if buf < 1024 {
		buf = 1024
	}

	writer := bufio.NewWriterSize(dstFile, buf)
	n, err := writer.ReadFrom(srcFile)
	if err != nil {
		_ = log.Warn("[transfer] ", server.SshIp, " file: ", remoteDir+file, " ", err.Error())
		return err
	}
	err = writer.Flush()
	if err != nil {
		_ = log.Warn("[transfer] ", server.SshIp, " file flush: ", remoteDir+file, " ", err.Error())
		return err
	}

	log.Debug("[transfer] ", server.SshIp, " file: ", dstFile.Name(), " size: ", humanize.Bytes(uint64(n)))

	return err
}

func SentFile(server *config.HostList, localFile string, remoteDir string, varFile string, buf int, perm os.FileMode) (err error) {
	defer log.Flush()
	var srcFileRender string
	var renderErr error
	if checkFileType(localFile) == ".j2" {
		log.Debug("[sent] ", server.SshIp, " render: ", localFile)
		srcFileRender, renderErr = render.Render(server, varFile, localFile)
		if renderErr != nil {
			return renderErr
		}
	} else {
		srcFileRender = localFile
	}
	//stat_t := fileInfo.Sys().(*syscall.Stat_t)
	log.Trace("[sent] ", server.SshIp, " open: ", srcFileRender)
	srcFile, e := os.Open(srcFileRender)
	if e != nil {
		_ = log.Warn("[sent] ", server.SshIp, " open: ", srcFileRender, " ", e.Error())
		return e
	}
	_, file := filepath.Split(srcFileRender)
	log.Debug("[sent] ", server.SshIp, " file: ", remoteDir+"/"+file)
	dstFile, ce := server.SftpClient.Create(remoteDir + "/" + file)
	if ce != nil {
		_ = log.Warn("[sent] ", server.SshIp, " file: ", remoteDir+"/"+file, " ", ce.Error())
		return ce
	}
	log.Trace("[sent] ", server.SshIp, " chmod file: ", remoteDir+"/"+file, " ", perm.String())
	ce = server.SftpClient.Chmod(filepath.Join(remoteDir, file), perm)
	/*	ce = server.SftpClient.Chmod(remoteDir+"/"+fileInfo.Name(), fileInfo.Mode())
		ce = server.SftpClient.Chown(remoteDir+"/"+file, int(stat_t.Uid), int(stat_t.Gid))*/

	if ce != nil {
		_ = log.Warn("[sent] ", server.SshIp, " chmod file: ", remoteDir+"/"+file, " ", perm.String(), " ", ce.Error())
		return ce
	}

	defer func(srcFile *os.File) {
		err1 := srcFile.Close()
		if err1 != nil {
			_ = log.Warn("[sent] ", server.SshIp, " close file: ", srcFile.Name(), " ", err1.Error())
		}
	}(srcFile)

	defer func(dstFile *sftp.File) {
		err1 := dstFile.Close()
		if err1 != nil {
			_ = log.Warn("[sent] ", server.SshIp, " close file: ", dstFile.Name(), " ", err1.Error())
		}
	}(dstFile)

	if buf < 1024 {
		buf = 4096
	}

	writer := bufio.NewWriterSize(dstFile, buf)
	n, err := writer.ReadFrom(srcFile)
	if err != nil {
		_ = log.Warn("[sent] ", server.SshIp, " file: ", dstFile.Name(), " ", err.Error())
		return err
	}
	err = writer.Flush()
	if err != nil {
		_ = log.Warn("[sent] ", server.SshIp, " file flush: ", dstFile.Name(), " ", err.Error())
		return err
	}
	log.Debug("[sent] ", server.SshIp, " file: ", dstFile.Name(), " size: ", humanize.Bytes(uint64(n)))

	return err
}

func FetchAll(server *config.HostList, localFile string, remoteDir string, size int, exclude string) error {
	defer log.Flush()
	err := os.MkdirAll(remoteDir, 0755)
	if err != nil {
		return err
	}

	log.Trace("[fetch] ", server.SshIp, " stat info: ", localFile)
	fileInfo, e := server.SftpClient.Stat(localFile)
	if e != nil {
		_ = log.Warn("[fetch] ", server.SshIp, " stat info: ", localFile, " ", e.Error())
		return e
	}
	if !fileInfo.IsDir() {
		if common.Filter(strings.Split(exclude, ";"), fileInfo.Name()) {
			log.Debug("[fetch] ", server.SshIp, " exclude ", fileInfo.Name())
			return nil
		}
		err = FetchFile(server, localFile, remoteDir, size, fileInfo.Mode())
		if err != nil {
			return err
		}
		return nil
	}

	fileList, readDirErr := server.SftpClient.ReadDir(localFile)
	if readDirErr != nil {
		_ = log.Warn("[fetch] ", server.SshIp, " Read dir ", localFile, " ", readDirErr.Error())
		return readDirErr
	}

	for _, file := range fileList {
		if common.Filter(strings.Split(exclude, ";"), file.Name()) {
			log.Debug("[fetch] ", server.SshIp, " exclude ", file.Name())
			continue
		}

		if file.IsDir() {
			log.Debug("[fetch] localhost create dir: ", path.Join(remoteDir, file.Name()))
			err = os.MkdirAll(path.Join(remoteDir, file.Name()), file.Mode())
			if err != nil {
				return err
			}
			log.Trace("[fetch] localhost chmod dir: ", path.Join(remoteDir, file.Name()), " ", file.Mode().String())
			err = os.Chmod(path.Join(remoteDir, file.Name()), file.Mode())
			if err != nil {
				_ = log.Warn("[fetch] localhost chmod dir: ", path.Join(remoteDir, file.Name()), file.Mode().String(), " ", err.Error())
				continue
			}
			err = FetchAll(server, path.Join(localFile, file.Name()), path.Join(remoteDir, file.Name()), size, exclude)
			if err != nil {
				continue
			}
		} else {
			err = FetchFile(server, path.Join(localFile, file.Name()), remoteDir, size, file.Mode())
			if err != nil {
				_ = log.Warn("[fetch] localhost FetchFile: ", path.Join(localFile, file.Name()), " ", err.Error())
				continue
			}
		}
	}

	return nil
}

func FetchFile(server *config.HostList, localFile string, remoteDir string, buf int, perm os.FileMode) error {
	defer log.Flush()
	log.Trace("[fetch] ", server.SshIp, " open file: ", localFile)
	srcFile, err := server.SftpClient.Open(localFile)
	if err != nil {
		_ = log.Warn("[fetch] ", server.SshIp, " open file: ", localFile, " ", err.Error())
		return err
	}

	_, file := filepath.Split(localFile)
	log.Debug("[fetch] localhost create file ", remoteDir+"/"+file)
	dstFile, err := os.Create(remoteDir + "/" + file)
	if err != nil {
		_ = log.Warn("[fetch] localhost create file ", remoteDir+"/"+file, " ", err.Error())
		return err
	}

	log.Trace("[fetch] localhost chmod file: ", dstFile.Name(), " ", perm.String())
	err = dstFile.Chmod(perm)
	if err != nil {
		_ = log.Warn("[fetch] localhost chmod file: ", dstFile.Name(), " ", perm.String(), " ", err.Error())
		return err
	}

	defer func(dstFile *os.File) {
		err1 := dstFile.Close()
		if err1 != nil {
			_ = log.Warn("[fetch] localhost close file failed ", dstFile.Name())
		}
	}(dstFile)

	defer func(srcFile *sftp.File) {
		err1 := srcFile.Close()
		if err1 != nil {
			_ = log.Warn("[fetch] ", server.SshIp, " close file failed ", srcFile.Name())
		}
	}(srcFile)

	if buf < 1024 {
		buf = 4096
	}
	log.Debug("[fetch] ", server.SshIp, " -> localhost file ", srcFile.Name())
	writer := bufio.NewReaderSize(srcFile, buf)
	n, err := writer.WriteTo(dstFile)
	if err != nil {
		_ = log.Warn("[fetch] ", server.SshIp, " -> localhost file ", srcFile.Name(), " ", err.Error())
		return err
	}
	log.Debug("[fetch] ", server.SshIp, " -> localhost file ", srcFile.Name(), " size: ", humanize.Bytes(uint64(n)))
	return err
}

func IsFile(file string) (bool, error) {
	defer func() {
		if RunErr := recover(); RunErr != nil {
		}
	}()

	if fd, err := os.Stat(file); err != nil {
		return false, err
	} else {
		return fd.IsDir(), err
	}

}

func TaskFileSync(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	for i := range yaml.HostList {
		swg.CPUChannel <- struct{}{}

		if !yaml.HostList[i].Include {
			swg.HostWg.Done()
			<-swg.CPUChannel
			continue
		}
		go runRsync(tasks, swg, &yaml.HostList[i])

	}
	swg.HostWg.Wait()
	log.Flush()
}

func runRsync(tasks *config.Task, swg *config.SyncWaitGroupChannelType, server *config.HostList) {
	defer log.Flush()
	config.CreateClient(server)
	Status := "Success"
	syncErr := ""
	var cmd *exec.Cmd

	for _, Files := range tasks.Rsync {
		var cmdString bytes.Buffer

		if Files.Dst == "" || Files.Src == "" {
			syncErr = errors.New(Files.Name + " value cannot be null").Error()
			Status = "Failed"
			goto failed
		}
		log.Debug("rsync: ", server.SshIp, " ", Files.Dst)

		if server.SshType == "password" {
			cmdString.WriteString("sshpass -p ")
			cmdString.WriteString(server.SshPw)
		} else if server.SshKeyPath != "" {
			cmdString.WriteString("ssh -i ")
			cmdString.WriteString(server.SshKeyPath)
		}
		cmdString.WriteString(" rsync -topgvrz  --timeout=10 ")

		if Files.BwLimit != "" {
			cmdString.WriteString(" --bwlimit=" + Files.BwLimit)
		}

		if Files.Sudo {
			cmdString.WriteString(" --rsync-path=\"sudo rsync\" ")
		}

		if Files.Delete == "yes" {
			cmdString.WriteString(" --delete ")
		}

		if Files.Exclude != "" {
			subFile := CmdSpilt(Files.Exclude, ",")
			for _, excludeFile := range subFile {
				cmdString.WriteString(" --exclude " + excludeFile)
			}
		}

		cmdString.WriteString(" -e 'ssh -p  " + strconv.Itoa(server.SshPort) + " -o PubkeyAuthentication=yes -o stricthostkeychecking=no' ")
		cmdString.WriteString(Files.Src + " " + server.SshUser + "@" + server.SshIp + ":" + Files.Dst)

		cmd = exec.Command("/bin/bash", "-c", cmdString.String())
		log.Trace("rsync: ", cmdString.String())

		if err := cmd.Start(); err != nil {
			Status = "Failed"
			syncErr = err.Error()
		}

		if err := cmd.Wait(); err != nil {
			Status = "Failed"
			syncErr = err.Error()
		}
	failed:
		cmdResult := struct {
			EName  string
			Rsync  config.Rsync
			Error  string
			Status string
		}{
			EName:  Files.Name,
			Rsync:  Files,
			Error:  syncErr,
			Status: Status,
		}

		res, err := json.Marshal(cmdResult)
		check(err)
		var out bytes.Buffer
		err = json.Indent(&out, res, "", " ")
		check(err)

		if Status == "Failed" {
			_ = log.Error("[", server.SshIp, "] => \n", out.String(), "\n")
		} else {
			log.Info("[", server.SshIp, "] => \n", out.String(), "\n")
		}

	}
	swg.HostWg.Done()
	<-swg.CPUChannel
}

func TaskCmd(tasks *config.Task, yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	for i, server := range yaml.HostList {
		swg.CPUChannel <- struct{}{}
		if !server.Include {
			swg.HostWg.Done()
			<-swg.CPUChannel
			continue
		}
		go runCmd(tasks, swg, &yaml.HostList[i])
	}
	swg.HostWg.Wait()
}
func runCmd(tasks *config.Task, swg *config.SyncWaitGroupChannelType, server *config.HostList) {
	for _, C := range tasks.Shell {

		for _, c := range C.Cmd {

			config.CreateClient(server)
			var err error
			if server.Session, err = CreateSession(server); err != nil {
				continue
				//panic(err)
			}

			log.Trace(server.SshIp, " command: ", c)
			var Status, outs string
			outs, runErr := RunShell(server.Session, server, c)
			if runErr != nil {
				Status = "Failed"
			} else {
				Status = "Success"
			}

			if outs == "" {
				if runErr != nil {
					_ = log.Error("[", server.SshIp, "] ", Status, " => \ncmd# ", c, "\n")
				} else {
					log.Info("[", server.SshIp, "] ", Status, " => \ncmd# ", c, "\n")
				}
				continue
			}

			if runErr != nil {
				_ = log.Error("[", server.SshIp, "] ", Status, " => \n", outs, "\n")

			} else {
				log.Info("[", server.SshIp, "] ", Status, " => \n", outs, "\n")
			}
		}
		log.Flush()
	}
	swg.HostWg.Done()
	log.Flush()
	<-swg.CPUChannel
}
func ConsoleCmd(yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	config.CreateClientAll(yaml)

	for {
		fmt.Printf("command# ")
		cmdReader := bufio.NewReader(os.Stdin)

		cmd, err := cmdReader.ReadString('\n')
		if err != nil {
			fmt.Printf("Found an error : %s\n", err)
			continue
		}

		cmd = strings.TrimSpace(cmd[:len(cmd)-1])
		if len(cmd) == 0 {
			continue
		}

		switch cmd {
		default:
			yaml.Task[0] = config.Task{
				Shell: []config.Shell{{
					Name: "cmd",
					Sudo: false,
					Cmd:  CmdSpilt(cmd, ";")}},
			}
			yaml.Task = yaml.Task[0:1]

			TaskCmd(&yaml.Task[0], yaml, swg)

		case "ping":
			PingHost(yaml, swg)
		case "exit":
			fmt.Println("㠭")
			return
		case "list":
			HostList(yaml)
		case "listall":
			HostListAll(yaml)
		case "remove":
			HostRemove(yaml)
		case "add":
			HostAdd(yaml)
		case "help":
			ConsoleHelp()
		}

	}
}

func PingHost(yaml *config.Yaml, swg *config.SyncWaitGroupChannelType) {
	defer log.Flush()
	swg.HostWg.Add(len(yaml.HostList))

	Time, _ := time.ParseDuration("3s")

	type result struct {
		Func   string
		Result string
		Status string
	}

	for i := range yaml.HostList {
		swg.CPUChannel <- struct{}{}
		go func(yaml0 *config.Yaml, swg *config.SyncWaitGroupChannelType, hostNum int) {
			var addr bytes.Buffer
			addr.WriteString(yaml0.HostList[hostNum].SshIp + ":" + strconv.Itoa(yaml0.HostList[hostNum].SshPort))

			if _, errPing := net.DialTimeout("tcp", addr.String(), Time); errPing != nil {

				cmdResult := result{
					Func:   "ping",
					Result: errPing.Error(),
					Status: "Failed",
				}
				res, err := json.Marshal(cmdResult)
				check(err)
				var out bytes.Buffer
				err = json.Indent(&out, res, "", " ")
				check(err)

				_ = log.Error(yaml.HostList[hostNum].SshIp, " => \n", out.String(), "\n")
			} else {
				cmdResult := result{
					Func:   "ping",
					Status: "Success",
				}
				res, err := json.Marshal(cmdResult)
				check(err)
				var out bytes.Buffer
				err = json.Indent(&out, res, "", " ")
				check(err)

				log.Info(yaml.HostList[hostNum].SshIp, " => \n", out.String(), "\n")
			}

			swg.HostWg.Done()
			<-swg.CPUChannel
		}(yaml, swg, i)

	}
	swg.HostWg.Wait()
}

func HostList(yaml *config.Yaml) {
	for i, server := range yaml.HostList {
		if server.Include {
			hostDesc := hostDesc{
				Ip:       server.SshIp,
				HostName: server.SshName,
				Group:    server.SshGroup,
				User:     server.SshUser,
				SshPort:  server.SshPort,
			}
			log.Info("hostList[", i, "]: ", hostDesc, "\n")
		}
	}
	log.Flush()
}

func HostListAll(yaml *config.Yaml) {
	defer log.Flush()
	for i, server := range yaml.HostList {
		hostDesc := hostDesc{
			Ip:       server.SshIp,
			HostName: server.SshName,
			Group:    server.SshGroup,
			User:     server.SshUser,
			SshPort:  server.SshPort,
		}
		if server.Include {
			log.Info("hostList[", i, "]: ", hostDesc, "\n")
		} else {
			log.Info("hostList[", i, "]: ", hostDesc, " offline", "\n")
		}
	}
}

func CmdSpilt(c string, sep string) (s []string) {
	s = strings.Split(c, sep)
	return
}

func AddRoutineChannel(chNum int) (ch chan struct{}, nums int) {
	if chNum < (10 * runtime.NumCPU()) {
		ch = make(chan struct{}, chNum)
		return ch, chNum
	} else {
		ch = make(chan struct{}, 10*runtime.NumCPU())
		return ch, 10 * runtime.NumCPU()
	}
}

func HostRemove(yaml *config.Yaml) {
	fmt.Print("num：")
	hostNum := bufio.NewReader(os.Stdin)
	n, err := hostNum.ReadString('\n')
	if err != nil {
		fmt.Printf("Found an error : %s\n", err)
	} else {
		//去掉\n
		if runtime.GOOS == "windows" {
			n = n[:len(n)-2]
		} else {
			n = n[:len(n)-1]
		}
	}
	num, err := strconv.Atoi(n)
	if err != nil {
		fmt.Printf("Found an error : %s\n", err)
	}
	yaml.HostList[num].Include = false
	fmt.Println("")
}

func HostAdd(yaml *config.Yaml) {
	fmt.Print("num：")
	hostNum := bufio.NewReader(os.Stdin)
	n, err := hostNum.ReadString('\n')
	if err != nil {
		fmt.Printf("Found an error : %s\n", err)
	} else {
		//去掉\n
		if runtime.GOOS == "windows" {
			n = n[:len(n)-2]
		} else {
			n = n[:len(n)-1]
		}
	}
	num, err := strconv.Atoi(n)
	if err != nil {
		fmt.Printf("Found an error : %s\n", err)
	}

	yaml.HostList[num].Include = true
	fmt.Println("")
}

func checkFileType(filepath string) (filetype string) {
	return path.Ext(filepath)
}

func check(e error) {
	if e != nil {
		panic(e)
	}
}

func ConsoleHelp() {
	fmt.Println("  help：    帮助")
	fmt.Println("  list：    主机列表，标识状态")
	fmt.Println("  listall： 所有主机")
	fmt.Println("  remove：  移除主机<以编号为索引>")
	fmt.Println("  add：     添加主机<以编号为索引>")
	fmt.Println("  ping：    判断有效主机连通性")
	fmt.Println("  exit：    退出操作")
	fmt.Println("")
}

func CreateSession(cliConf *config.HostList) (session *ssh.Session, err error) {
	defer log.Flush()
	defer func() {
		if err := recover(); err != nil {
		}
	}()

	if session, err = cliConf.Client.NewSession(); err != nil {
		var bt bytes.Buffer
		bt.WriteString(cliConf.SshIp + " " + err.Error())
		_ = log.Errorf(bt.String())
	}

	return session, err
}

func RunShell(session *ssh.Session, cliConf *config.HostList, shell string) (output string, RunErr error) {
	defer func() {
		if RunErr := recover(); RunErr != nil {
		}
	}()

	var result []byte
	result, RunErr = session.CombinedOutput(shell)
	if len(result) != 0 {
		cliConf.LastResult = string(result)[:len(result)-1]
	} else {
		cliConf.LastResult = string(result)
	}

	return cliConf.LastResult, RunErr
}

func hostEnv(g, ip string) []string {
	i := 0
	k := g + "." + ip
	l := len(config.HostVars[k].(map[string]interface{}))
	s := make([]string, l, l)
	for key, val := range config.HostVars[k].(map[string]interface{}) {
		s[i] = key + "=" + val.(string)
		i++
	}
	return s
}
